跳到主要内容

SpringSecurity授权服务器、资源服务器

授权服务器

注意:之前的授权服务器框架 Spring security oauth2 官方已经不再维护了 参考以下资料 为什么你使用的 Spring Security OAuth 过期了?

所以这篇文章主要还是介绍旧的 Spring security oauth2 资源服务器

资源服务器是围绕着授权服务器进行的,在 oauth2 中当有了授权服务器以后,才会有资源服务器

先来了解几个概念

  • Authorize Endpoint:授权端点,进行授权
  • Token Endpoint:令牌端点,经过授权拿到对应的 Token
  • Introspection Endpoint:校验端点,校验 Token 的合法性
  • Revocation Endpoint:撤销端点,撤销授权

image.png

添加依赖

<!-- 这里使用的是 spring-cloud-starter-security 不是 SpringBoot的 -->
<!-- 挺神奇当的,默认的 springboot-oauth2 的一些方法注解是过期的,
但是 spring-cloud-starter-oauth2 却没有-->
<properties>
<java.version>1.8</java.version>
<!-- 注意 SpringBoot 的版本要对的上 -->
<!-- 参考:https://spring.io/projects/spring-cloud#overview -->
<spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- 一些其他的基本依赖就省了(例如 SpringBoot 启动器) -->
</dependencies>

配置 Security

自定义一个 UserDetails 实现类

public class MyUser implements UserDetails {
private final String password;

private final String username;

private final List<GrantedAuthority> authorities;

public MyUser(String password, String username, List<GrantedAuthority> authorities) {
this.password = password;
this.username = username;
this.authorities = authorities;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// 略,总之把 false 都改成 true
}

配置登陆服务 UserDetailsService

@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("admin");
return new MyUser(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}

配置 Security 认证

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 这里放行了 oauth,其是专门用来授权的接口
.antMatchers("/oauth/**","/login/**","/logout/**").permitAll()
.anyRequest().authenticated()
.and()
// 表单请求放行
.formLogin().permitAll()
.and()
.csrf().disable();
}
}

配置授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 这个 clients 指的是请求的那个客户端
clients
// 写到内存
.inMemory()
// 客户端 ID
.withClient("client")
// 密钥(这里需要加密一下)
.secret(passwordEncoder.encode("this_is_secret_key"))
// 重定向地址
.redirectUris("https://alsritter.icu/")
// 授权范围
.scopes("all")
// 授权类型 authorization_code:授权码模式
.authorizedGrantTypes("authorization_code");

}
}

资源服务器

配置资源服务器

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").permitAll()
.anyRequest().authenticated();
}
}

测试 Controller

@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
// 这个返回的实际上就是自定义登陆逻辑里的 User 类
return authentication.getPrincipal();
}
}

authorizeRequests 顺序

参考资料 老大差点拿我祭旗!Spring Security authorizeRequests顺序问题不容忽视

authorizeRequests() 所创建的 RequestMatcher 是有顺序的。最终的 OrRequestMatcher 逻辑则是,谁先匹配,就交给谁。

所以一般都是把 anyRequest().authenticated() 放在最后面

requestMatchers 的作用

参考资料 Spring requestMatchers, antMatchers and authorizeRequests usage

这个 requestMatchers 单独拿出来用毫无意义,一般是搭配 authorizeRequests 使用的

http
.requestMatchers().antMatchers("/", "/login")
.and()
.authorizeRequests().anyRequest().authenticated();

上述的代码表示允许 //login 直接通过,就如上面 authorizeRequests 顺序 那节所说,先匹配就交给谁。

资源服务器过滤器的顺序

参考资料 Spring 的 WebSecurityConfigurerAdapter 过滤器

配置资源服务器时会发现在 ResourceServerConfigurerAdapter 上配置了访问控制,而之前在 WebSecurityConfigurerAdapter 也配置了访问控制,所以这两个的校验谁先谁后呢?

点开 WebSecurityConfigurerAdapter 可以发现其默认的 Order 设置为 100

@Order(100)
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
...

ResourceServerConfigurerAdapter 则是 3

@Configuration
public class ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered {

private int order = 3;
...

在 Spring 里 Order 值越小优先级越高,所以 ResourceServerConfigurerAdapter 优先级比另外一个更高,他会优先处理。

测试授权码模式

这个测试是基于上面的配置的

获取授权码

先获取授权码

http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=https://alsritter.icu/&scope=all

上面的链接解释一下

  1. oauth/authorize 授权服务器(这个地址是指定的)
  2. response_type=code 表示期望获取授权码
  3. client_id=client 客户端id(要和授权服务器的一样)
  4. redirect_uri 重定向的地址
  5. scope 作用域

登陆后会跳转到如下页面

image.png 它会询问你是否允许 client 访问被保护的资源

点击允许后会重定向到之前指定的链接,且返回一个 code

# 重定向到如下地址

https://alsritter.icu/?code=XCMxjE
# 因为上面 redirect_uri 我填的是我的博客,所以会重定向到这里
# 关键是看后面跟着的这个 code=XCMxjE 这个就是授权码

获取令牌

获取授权码后进行下一步

先在 Authorization 输入 username 和 password,这里的 username 就是上面的客户端 id,密码就是那个密钥 image.png

然后再在表单添加

image.png

这个请求的请求头如下(这个 Authorization 字段后面的内容是通过 Base64 编码过的,解码后其内容为 client:this_is_secret_key

Authorization: Basic Y2xpZW50OnRoaXNfaXNfc2VjcmV0X2tleQ==
User-Agent: PostmanRuntime/7.26.5
Accept: */*
Cache-Control: no-cache
Postman-Token: 1f7335a2-6096-4a3d-b1a1-1de82ea6f9b8
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 112

请求体如下

grant_type: "authorization_code"
client_id: "client"
redirect_uri: "https://alsritter.icu/"
code: "XCMxjE"
scope: "all"

最后就取得了 Token

{
"access_token": "2f58098c-af89-4d7c-b670-5e02d5f9d9bd",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all"
}

使用 Token 获取资源

使用这个 Token 去获取资源服务器的资源 image.png

请求头为

Authorization: Bearer 2f58098c-af89-4d7c-b670-5e02d5f9d9bd
User-Agent: PostmanRuntime/7.26.5
Accept: */*
Cache-Control: no-cache
Postman-Token: 00784871-7b05-4ef3-ad68-fb82c392b70f
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

密码模式

添加配置

先在 WebSecurityConfigurerAdapter 注入 AuthenticationManager

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
...

然后再在授权服务器加上这个端点

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
...

然后别忘记在授权类型里加上这个密码模式

// 支持多种模式
.authorizedGrantTypes("authorization_code","password");

获取令牌

把上面的改成如下(别忘了加上 Authorization)

image.png

请求头

Authorization: Basic Y2xpZW50OnRoaXNfaXNfc2VjcmV0X2tleQ==
User-Agent: PostmanRuntime/7.26.5
Accept: */*
Cache-Control: no-cache
Postman-Token: 2fab209a-d3ca-460c-82e7-82e53a539cd5
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 59

请求体

grant_type: "password"
scope: "all"
username: "admin"
password: "admin"

JWT 整合到 Oauth

官方文档

默认 Security 采用的是 Cookie 认证机制,搭配服务端的 Session 使用;通过客户端带上来的 Cookie 对象与服务端的 Session 对象匹配来实现状态管理(默认每当关闭浏览器时 Cookie 都会被删除,如果需要保留一段时间可以使用 Cookie 的 expire time 使 Cookie 存留一段时间)

JWT 的具体细节就不再赘述了,之前那篇 JWT 使用方法那里已经讲过了,这里使用这个 JWT 的原因是因为其能 使资源服务器不再依赖认证服务器即可完成授权,但是 JWT 令牌还是有个占用存储空间大的缺点的

整合到 Oauth

先把 JWT 的 Store 注入

@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
// 创建一个转换器
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 设置 jwt 密钥
jwtAccessTokenConverter.setSigningKey("test_key");
return jwtAccessTokenConverter;
}
}

还是像上面配置 Redis 那样在 AuthorizationServerConfigurerAdapter 配置这个存储模式,不过这次不用再使用 Redis 了

@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
// 需要使用之前注入的转换器(将 accessToken 转成 JwtToken)
.accessTokenConverter(jwtAccessTokenConverter);
}

然后再次访问授权服务器就能看到返回的 Token 变成了 JWT

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU4MTA5MjQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiODNmMjY1NTgtZTc0NS00MTRlLWIwNmUtMjE2OGUwYjJiYzYxIiwiY2xpZW50X2lkIjoiY2xpZW50Iiwic2NvcGUiOlsiYWxsIl19.Zbs58qRX5lHYoBROV_m41nthDRoaqoDtQBfTcMQuvaM",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all",
"jti": "83f26558-e745-414e-b06e-2168e0b2bc61"
}

把这个 Token 粘贴到上面的 JWT 官网的解析器可以看到结果如下

image.png

拓展 JWT 里内容

如果直接在 JWT 添加内容不复杂,但是使用了 Security 框架拓展就比较麻烦了

这里增加内容的原理就是先在 OAuth2AccessToken 接口默认的实现类 DefaultOAuth2AccessToken 里面添加内容,然后再在配置类 AuthorizationServerConfigurerAdapter 里给这些添加的部分也指定为 JWT 的转换器。

先创建一个增强类

public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> map = new HashMap<>();
map.put("myInfo","this is my info");
// 强转成 DefaultOAuth2AccessToken,这个类是 OAuth2AccessToken 接口的默认实现类,
// 如果要添加内容一般都是先转成这个类,再通过它的 setAdditionalInformation 方法
// 添加自定义的内容,最后在配置类那里为这个 TokenEnhancer 指定一个相应的转换器
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);
return accessToken;
}
}

然后再在 JwtTokenStoreConfig 注入这个增强器

@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}

AuthorizationServerConfigurerAdapter 配置这个增强器

@Autowired
@Qualifier("jwtTokenEnhancer")
private TokenEnhancer tokenEnhancer;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 设置 JWT 增强的内容
TokenEnhancerChain chain = new TokenEnhancerChain();

List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(tokenEnhancer);
// 因为上面本质也只是先在 accessToken 上添加内容,所以这里也要指定其转换器
delegates.add(jwtAccessTokenConverter);
chain.setTokenEnhancers(delegates);

endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
// 需要使用之前注入的转换器(将 accessToken 转成 JwtToken)
.accessTokenConverter(jwtAccessTokenConverter)
// 使用增强器
.tokenEnhancer(chain);
}

再次去请求这个授权服务器返回值可以看到这里多了个 myInfo 字段

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsIm15SW5mbyI6InRoaXMgaXMgbXkgaW5mbyIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MDU4MTMyMzMsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjViZDBmMTA4LThmOWEtNDhjZS1hZTliLWU1ZTQwNzkwZGQ1YyIsImNsaWVudF9pZCI6ImNsaWVudCJ9.t7zQ2_NHy0UtB38yxOuDGUgeyezSQ9_jR7HWidMz3PE",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all",
"myInfo": "this is my info",
"jti": "5bd0f108-8f9a-48ce-ae9b-e5e40790dd5c"
}

丢去解析可以看到在 JWT 里面也多了这个 myInfo 字段 image.png

解析 JWT 内容

先引入 JJWT 的依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

就是读取请求头的 Authorization 字段

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsIm15SW5mbyI6InRoaXMgaXMgbXkgaW5mbyIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MDU4MTMyMzMsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjViZDBmMTA4LThmOWEtNDhjZS1hZTliLWU1ZTQwNzkwZGQ1YyIsImNsaWVudF9pZCI6ImNsaWVudCJ9.t7zQ2_NHy0UtB38yxOuDGUgeyezSQ9_jR7HWidMz3PE

在 Controller 里可以取得这个 JWT 并解析

@GetMapping("/getCurrentUser")
public Object getCurrentUser(HttpServletRequest request) {
String header = request.getHeader("Authorization");
// 因为 indexOf 返回的是第一次出现的位置,而 bearer 为 6,其后面跟一个空格,因此从第七个开始取
String token = header.substring(header.indexOf("bearer") + 7);
return Jwts.parser()
// 这里后面的部分是为了防止乱码
.setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}

刷新令牌

参考资料 问题备忘:Spring Cloud OAuth2.0 开发过程中碰到的问题

先在 AuthorizationServerConfigurerAdapter 配置令牌失效时间(注意这里要加上刷新令牌)

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 这个 clients 指的是请求的那个客户端
clients
// 写到内存
.inMemory()
// 客户端 ID
.withClient("client")
// 密钥(用来请求授权服务器的)
.secret(passwordEncoder.encode("this_is_secret_key"))
// 授权范围
.scopes("all")
// 授权类型为密码模式,然后使用刷新令牌
.authorizedGrantTypes("password", "refresh_token")
// 设置失效时间为 10 分钟
.accessTokenValiditySeconds(60 * 10)
// 设置刷新令牌过期时间
.refreshTokenValiditySeconds(60 * 60 * 24 * 7);
}

返回值如下,可见多了一个刷新令牌

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsIm15SW5mbyI6InRoaXMgaXMgbXkgaW5mbyIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MDU3NzMzODgsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjU2YmUyZDc0LWVhM2QtNDc0YS1iZTI5LTUzYmQ3YTlmODgzOSIsImNsaWVudF9pZCI6ImNsaWVudCJ9.XFoevQ5_k5D3qCubcbxIVJyfqX1bH_ZE7yWxBPcA-g0",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsIm15SW5mbyI6InRoaXMgaXMgbXkgaW5mbyIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1NmJlMmQ3NC1lYTNkLTQ3NGEtYmUyOS01M2JkN2E5Zjg4MzkiLCJleHAiOjE2MDYzNzc1ODgsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjYxOWIyMzZiLTRmNGUtNGIwZi1hZWM4LThhODE2OTA3OGUwNSIsImNsaWVudF9pZCI6ImNsaWVudCJ9.J91Db2VSYvbd_PohIHT5nlFCzqlfvZmlMCEvZYBt1WI",
"expires_in": 599,
"scope": "all",
"myInfo": "this is my info",
"jti": "56be2d74-ea3d-474a-be29-53bd7a9f8839"
}

使用这个刷新令牌和之前的请求令牌大同小异

image.png

image.png

注意:在passwod模式下,执行刷新token时,抛出异常Handling error: IllegalStateException, UserDetailsService is required. 解决方法: 在 AuthorizationServerConfigurerAdapter 中配置 UserDetailsService 对象 如下

@Autowired
@Qualifier("myUserService")
private UserDetailsService userDetails;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
...

// 这里配置上自定义的 UserDetailsService
endpoints.userDetailsService(userDetails)
.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
// 需要使用之前注入的转换器(将 accessToken 转成 JwtToken)
.accessTokenConverter(jwtAccessTokenConverter)
// 使用增强器
.tokenEnhancer(chain);
}

Redis 存储 Token

配置环境

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

整合到密码模式

TokenStore 注入进来

@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}

AuthorizationServerConfigurerAdapter 配置这个存储模式,表示存储模式是使用 Redis

@Autowired
@Qualifier("redisTokenStore")
private TokenStore tokenStore;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore);
}

这样启动时就会自动把令牌信息存到 Redis 里面去了,而不是继续使用 Session

RememberMe

故名思意,就是 “记住我” 下次就可以免登陆了,因为这个功能底层是依赖于 Spring-JDBC 的,所以最好使用 MyBatis 框架

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>

配置连接池

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf8&useSSL=true&useServerPrepStmts=true
username: root
password: root

在启动类加上注解

添加配置

在配置之前先来了解下这个 PersistentTokenRepository 接口,这个接口的实现是用来存储 RememberMe 的信息

public interface PersistentTokenRepository {

void createNewToken(PersistentRememberMeToken token);

void updateToken(String series, String tokenValue, Date lastUsed);

PersistentRememberMeToken getTokenForSeries(String seriesId);

void removeUserTokens(String username);

}

其有两个默认的实现类 JdbcTokenRepositoryImplInMemoryTokenRepositoryImpl

InMemoryTokenRepositoryImpl 是把用户信息存在内存中 JdbcTokenRepositoryImpl 则是存储到数据库中

将这个 PersistentTokenRepository 注入到 Bean

@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建一个表(第一次启动时开启,第二次注释掉)
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}

添加 remember-me

@Autowired
private PersistentTokenRepository persistentTokenRepository;


@Autowired
private UserDetailService userDetailsService;

...

http
.rememberMe()
// 因为默认的参数名是 remember-me,而前端不支持名字里带 - 所以这里重命名为 remember_me
.rememberMeParameter("remember_me")
// 存放的数据源位置
.tokenRepository(persistentTokenRepository)
// 设置超时时间,默认是两周
.tokenValiditySeconds(60 * 10)
// 自定义的登陆逻辑
.userDetailsService(userDetailsService);

前端测试

使用前端端分离,基本配置和之前一样,这里加个 rememberMe

<div>
<span>记住我:</span>
<input type="checkbox" v-model='remember_me'>
</div>
axios.post('/doLogin', qs.stringify({
username: this.username,
password: this.password,
remember_me: this.remember_me
})

点击 “记住我” 登陆后就会在数据库里插入一个 Token

image.png

然后下次服务器重启就可以无需登陆,可以直接使用需要权限的请求了(退出登陆也会自动把表里的 Token 清除掉)

注意:别忘了运行一次后把 jdbcTokenRepository.setCreateTableOnStartup(true); 注释掉,不然会报错